#ifndef ARRAY_HPP
#define ARRAY_HPP

#include <iostream>

using namespace std;

namespace cs540{

class OutOfRange{
	public:
		OutOfRange(){}
};

template <int D, typename T, typename U>
class ArrPtr
{
	public:
		typedef typename ArrPtr<D-1,T,U>::ptr* ptr; 
};

template <typename T,typename U>
class ArrPtr<0, T, U>
{
	public:
		typedef T ptr;		/* typedef T to ptr, If you dont do this then the type will become something like 'T ptr **** p'. 
					*  So to set actually data-type here we need to typedef it, e.g typedef T ptr == typedef int ptr == ptr **** p;
					*/
};

template <int D, typename T, typename U>
class AllocArray{
	public:
		AllocArray() { cout << "AllocArray ctor\n";}
		~AllocArray(){ cout << "AllocArray dtor\n";}

		static typename ArrPtr<D, T, T>::ptr get_memory(size_t *dims){
			
			typename ArrPtr<D,T,T>::ptr t_ptr;
			size_t i;

			t_ptr = new typename ArrPtr <D-1,T,T>::ptr [dims[0]];
			for (i = 0; i < dims[0]; i++){
				t_ptr[i] = AllocArray <D-1,T,T>::get_memory(dims+1); 
			}

			return t_ptr;
		}

		static void free_memory (typename ArrPtr <D,T,T>::ptr arr_ptr, size_t *dims){
			size_t i;
			for (i = 0; i < dims[0] ; i++){
				AllocArray<D-1, T,T>::free_memory (arr_ptr[i], dims+1);
			}
			
			delete [] arr_ptr;
		}

		static void copy_array (typename ArrPtr<D,T,T>::ptr dst, typename ArrPtr<D,U,U>::ptr src, size_t *dims){
			size_t i;
			for (i = 0; i < dims[0]; i++){
				AllocArray<D-1, T,U>::copy_array(dst[i], src[i], dims+1);
			}
		}
		
};

template <typename T,typename U>
class AllocArray<1,T,U>{
	public:
		static typename ArrPtr<1,T,T>::ptr get_memory (size_t *dims){
			typename ArrPtr<1,T,T>::ptr t_ptr;
			t_ptr = new T [dims[0]];
			return t_ptr;
		}

		static void free_memory (typename ArrPtr <1,T,T>::ptr arr_ptr, size_t *dims){
			if (arr_ptr)
				delete [] arr_ptr;
			arr_ptr = NULL;	
		}	

		static void copy_array (typename ArrPtr<1,T,T>::ptr dst, typename ArrPtr<1,U,U>::ptr src, size_t *dims){
			size_t i;
			typename ArrPtr<1,T,T>::ptr t_dst = dst;
			typename ArrPtr<1,U,U>::ptr t_src = src;
			
			for (i = 0; i < dims[0]; i++){
				*t_dst = *t_src;
				t_dst ++;
				t_src ++;
			}
		}	
};

template<int D,typename T>
class IndexHelper{
	public:
		typename ArrPtr<D,T,T>::ptr  base;	// int **** base;
		typename ArrPtr<D+1,T,T>::ptr  tmp;	// int **** base;
		size_t range_index;
		size_t *t_dims;
	public:
		
		IndexHelper(){}
		IndexHelper(typename ArrPtr<D,T,T>::ptr a) : base(a){}

		IndexHelper<D-1,T> operator[](size_t index){
			IndexHelper<D-1,T> a; //= new Array<D,T>;
			try{
				OutOfRange o;
				if (t_dims[range_index] <= index)
					throw o;
			}
			catch(...){
				throw;
			}

			a.base = *(this->base + index);
			a.range_index = range_index + 1;
			a.t_dims = t_dims;
			return a;
		}
};


template<typename T>
class IndexHelper<1,T>{
	
	public:
		typename ArrPtr<1,T,T>::ptr  base;	// int **** base;
		typename ArrPtr<2,T,T>::ptr  tmp;	// int **** base;
		size_t range_index;
		size_t *t_dims;
	
		T& operator[](size_t index){
			try{
				OutOfRange o;
				if (t_dims[range_index] <= index)
					throw o;
			}
			catch(...){
				throw;
			}
			return base[index];
		}
		
		T & operator= (T src){
			*base = src;
			return *base;
		}
};

template<typename T>
class IndexHelper<0,T>{
	
	public:
		typename ArrPtr<0,T,T>::ptr  base;	// int **** base;
		typename ArrPtr<1,T,T>::ptr  tmp;	
		size_t range_index;
		size_t *t_dims;
	
		T& operator[](size_t index){
			try{
				OutOfRange o;
				if (t_dims[range_index] <= index)
					throw o;
			}
			catch(...){
				throw;
			}
			return base[index];
		}
		
		T& operator= (T src){
			*tmp = src;
			return *tmp;
		}

		operator T(){
			return base;
		}
		
};


template <int D, typename T> 
class Array{
	public:
		typename ArrPtr<D,T,T>::ptr arr_ptr;
		int size;
		size_t *arr_dims;
		size_t dim;

	public:/* friend declaration */
		friend class IndexHelper<D,T>;
			
	public:
		Array() : arr_ptr(0), size(0), arr_dims(0), dim(0){}
		Array(size_t dims[]){
		
			/* get some memory for the array */
			arr_ptr = AllocArray<D,T,T>::get_memory(dims);
			arr_dims = new size_t[D];
			for (size_t i = 0; i < D; i++)
				arr_dims[i] = dims[i];
			dim = D;	
		}

		Array (const Array &src){

			arr_ptr = AllocArray<D,T,T>::get_memory (src.arr_dims);
			arr_dims = new size_t[src.dim];
			for (size_t i = 0; i < src.dim; i++)
				arr_dims[i] = src.arr_dims[i];
			dim = src.dim;	
			AllocArray<D,T,T>::copy_array (arr_ptr, src.arr_ptr, arr_dims);	
		}
		
		template <typename U>
		Array(const Array<D, U> &src){

			arr_ptr = AllocArray<D,T,U>::get_memory (src.arr_dims);
			arr_dims = new size_t[D];
			for (size_t i = 0; i < src.dim; i++)
				arr_dims[i] = src.arr_dims[i];
			dim = src.dim;	
			AllocArray<D,T,U>::copy_array (arr_ptr, src.arr_ptr, arr_dims);	
		}

		Array &operator=(const Array &src){
			
			if (this != &src){
				if (dim == src.dim){
					if (arr_ptr){
						AllocArray<D,T,T>::free_memory(arr_ptr, arr_dims);
						delete [] arr_dims;
					}

					arr_ptr = AllocArray<D,T,T>::get_memory (src.arr_dims);
					arr_dims = new size_t[src.dim];
					for (size_t i = 0; i < src.dim; i++)
						arr_dims[i] = src.arr_dims[i];
					dim = src.dim;	
					AllocArray<D,T,T>::copy_array (arr_ptr, src.arr_ptr, arr_dims);	
				}
				else{
					cout << "Invalid assignment" << endl;
				}
			}
			return *this;
		}

		template <typename U>
		Array &operator=(const Array<D,U> &src){
		
			if (dim == src.dim){
				if (arr_ptr){
					AllocArray<D,T,T>::free_memory(arr_ptr, arr_dims);
					delete [] arr_dims;
				}

				arr_ptr = AllocArray<D,T,U>::get_memory (src.arr_dims);
				arr_dims = new size_t[D];
				for (size_t i = 0; i < src.dim; i++)
					arr_dims[i] = src.arr_dims[i];
				dim = src.dim;	
				AllocArray<D,T,U>::copy_array (arr_ptr, src.arr_ptr, arr_dims);	

			}

			return *this;
		}

		~Array(){

			if (arr_dims){
				AllocArray<D,T,T>::free_memory(arr_ptr, arr_dims);
				delete [] arr_dims;
			}
		}

		IndexHelper<D-1,T> operator[](const size_t index){
			
			IndexHelper<D-1,T> a; //= new Array<D,T>;
			try{
				OutOfRange o;
				if (arr_dims[0] <= index)
					throw o;
			}
			catch(...){
				throw;
			}
			a.base = this->arr_ptr[ index];
			a.range_index = 1;
			a.tmp = this->arr_ptr;
			a.t_dims = arr_dims;
			return a;
		}
		
		const IndexHelper<D-1,T> operator[](const size_t index) const{
			
			try{
				OutOfRange o;
				if (arr_dims[0] <= index)
					throw o;
			}catch(...){
				throw;
			}
			IndexHelper<D-1,T> a; //= new Array<D,T>;
			a.base = *(this->arr_ptr + index);
			a.range_index = 1;
			a.t_dims = arr_dims;
			return a;
		}
};

}

#endif //ARRAY_HPP
